using System;
using System.Text;
using System.IO;
using System.Management.Automation;

namespace PSSymbolStore
{
    public class SymbolStore
    {
        public static readonly string FILE_PTR = "file.ptr";
        public static readonly string REFS_PTR = "refs.ptr";

        private const string PINGME_FILE = "pingme.txt";
        private const string ADMIN_DIR = "000Admin";
        private static readonly string HISTORY_FILE = ADMIN_DIR + Path.DirectorySeparatorChar + "history.txt";
        private static readonly string LASTID_FILE = ADMIN_DIR + Path.DirectorySeparatorChar + "lastid.txt";
        private static readonly string SERVER_FILE = ADMIN_DIR + Path.DirectorySeparatorChar + "server.txt";

        internal static readonly string FIRST_TRANSACTION_ID = "0000000001";

        private readonly Cmdlet _cmdlet;
        private readonly string _location;
        private bool _isValid;

        private readonly string _pingMeFile;
        private readonly string _adminDir;
        private readonly string _historyFile;
        private readonly string _lastIdFile;
        private readonly string _serverFile;

        public SymbolStore(Cmdlet cmdlet, string location)
        {
            if (location == null)
            {
                throw new ArgumentNullException("location");
            }

            if (cmdlet == null)
            {
                throw new ArgumentNullException("cmdlet");
            }

            if (location.Length == 0)
            {
                throw new ArgumentOutOfRangeException("location", "A symbol store location is required");
            }

            _cmdlet = cmdlet;
            _location = location;

            _pingMeFile = Path.Combine(_location, PINGME_FILE);
            _adminDir = Path.Combine(_location, ADMIN_DIR);
            _historyFile = Path.Combine(_location, HISTORY_FILE);
            _lastIdFile = Path.Combine(_location, LASTID_FILE);
            _serverFile = Path.Combine(_location, SERVER_FILE);
        }

        public Cmdlet Cmdlet
        {
            get { return _cmdlet; }
        }

        public string Location
        {
            get { return _location; }
        }

        public string PingMeFile
        {
            get { return _pingMeFile; }
        }

        public string AdminDir
        {
            get { return _adminDir; }
        }

        public string HistoryFile
        {
            get { return _historyFile; }
        }

        public string LastIdFile
        {
            get { return _lastIdFile; }
        }

        public string ServerFile
        {
            get { return _serverFile; }
        }

        public bool IsValid
        {
            get { return _isValid; }
        }

        public string GetTransactionDeletedFile(string transactionId)
        {
            return Path.Combine(_adminDir, String.Format("{0}.deleted", transactionId));
        }

        public string GetTransactionLockedFile(string transactionId)
        {
            return Path.Combine(_adminDir, String.Format("{0}.locked", transactionId));
        }

        public string GetTransactionFile(string transactionId)
        {
            return Path.Combine(_adminDir, transactionId);
        }

        internal string GetServerFilePath(string originalPath, SymbolFileInformation info, out TransactionFile transactionFile)
        {
            transactionFile = new TransactionFile
                                  {
                                      OriginalLocation = originalPath,
                                      SymbolStoreLocation =
                                          Path.Combine(Path.GetFileName(originalPath), info.UniqueIdentifier)
                                  };

            return Path.Combine(_location, transactionFile.SymbolStoreLocation);
        }

        public void Open()
        {
            Open(true);
        }

        public void Open(bool createIfNotExists)
        {
            if (!File.Exists(_pingMeFile))
            {
                if (createIfNotExists)
                {
                    // Create the symbol store
                    Create();
                }
                else
                {
                    // Unable to open symbol store
                    throw new SymbolStoreException("Unable to open symbol store at " + _location);
                }
            }

            _isValid = true;
        }

        public void Create()
        {
            if (Cmdlet.ShouldProcess(_location, "Create PSSymbolStore"))
            {
                // Create the store directory if it doesn't exist
                if (!Directory.Exists(_location))
                {
                    Directory.CreateDirectory(_location);
                }

                // Create the 000Admin subdirectory if it doesn't exist
                if (!Directory.Exists(_adminDir))
                {
                    Directory.CreateDirectory(_adminDir);
                }

                // Touch the PINGME file
                if (!File.Exists(_pingMeFile))
                {
                    StreamWriter sw = File.CreateText(_pingMeFile);
                    sw.Close();
                    sw.Dispose();
                }
            }
        }

        public string GetNextTransactionId()
        {
            string nextId = String.Empty;

            // Open the LASTID file and return the next number
            if (!File.Exists(_lastIdFile))
            {
                // The LASTID file doesn't exist, return the initial value
                Cmdlet.WriteDebug("Next transaction is: " + FIRST_TRANSACTION_ID);
                nextId = FIRST_TRANSACTION_ID;
            }

            bool lastIdFileExists = File.Exists(_lastIdFile);
            if (String.IsNullOrEmpty(nextId) && lastIdFileExists)
            {
                string lastId = File.ReadAllText(_lastIdFile);
                if (!String.IsNullOrEmpty(lastId))
                {
                    int id;
                    if (!Int32.TryParse(lastId, out id))
                    {
                        throw new SymbolStoreException("Unable to parse the last transaction ID");
                    }

                    do
                    {
                        // Increment and verify
                        id++;
                        nextId = id.ToString("D10", System.Globalization.CultureInfo.InvariantCulture);
                    }
                    // While the next ID has a valid file on the server, increment to find the next available ID
                    while (!IsValidNewTransactionId(nextId));
                }
                else
                {
                    nextId = FIRST_TRANSACTION_ID;
                }
            }
            else if (lastIdFileExists)
            {
                throw new SymbolStoreException("Unable to retrieve the next transaction ID", new FileNotFoundException("Unable to retrieve the next transaction ID", _lastIdFile));
            }

            Cmdlet.WriteDebug("Next transaction is: " + nextId);
            return nextId;
        }

        internal void SetLastTransactionId(string id)
        {
            Cmdlet.WriteDebug("Setting last transaction id: " + id);
            File.WriteAllText(_lastIdFile, id);
        }

        private bool IsValidNewTransactionId(string id)
        {
            bool transactionExists = (File.Exists(GetTransactionFile(id))
                        || File.Exists(GetTransactionDeletedFile(id))
                        || File.Exists(GetTransactionLockedFile(id)));

            // new ID is valid if the specified transaction doesn't exist
            return !transactionExists;
        }

        public bool CommitTransaction(Transaction transaction)
        {
            if (transaction == null)
            {
                throw new ArgumentNullException("transaction");
            }

            bool shouldProcess = false;

            var delTran = transaction as DeleteTransaction;
            if (delTran != null)
            {
                shouldProcess = Cmdlet.ShouldProcess(delTran.DeletedTransactionId, "Delete");
            }

            var addTran = transaction as AddTransaction;
            if (addTran != null)
            {
                shouldProcess = Cmdlet.ShouldProcess(addTran.TransactionId, "Add Symbols");
            }

            if (shouldProcess)
            {
                // Add transactions have additional requirements
                if (addTran != null)
                {
                    // Write the transaction file
                    var sb = new StringBuilder();
                    foreach (TransactionFile file in addTran.Files)
                    {
                        sb.AppendLine(file.ToServerString());
                    }

                    File.WriteAllText(GetTransactionFile(addTran.TransactionId), sb.ToString());
                }

                // Build the commit string
                Helpers.AppendLinesToFile(HistoryFile, transaction.ToString());
                
                if (addTran != null)
                {
                    // To conform to the existing SYMSTORE implementation,
                    // any writes to the server file should be appended with a newline character
                    Helpers.AppendLinesToFile(ServerFile, transaction + Environment.NewLine);
                }

                SetLastTransactionId(transaction.TransactionId);
            }

            return shouldProcess;
        }

        public override bool Equals(object obj)
        {
            // Do a case insensitive comparison
            var store = obj as SymbolStore;
            if (store != null)
            {
                return String.Equals(_location, store._location, StringComparison.InvariantCultureIgnoreCase);
            }
            return base.Equals(obj);
        }

        public override int GetHashCode()
        {
            return _location.GetHashCode();
        }

        public override string ToString()
        {
            return _location;
        }
    }
}
